<?php

namespace mongrove;

use \ArrayAccess;
use \Exception;

/**
 *
 * A Structure is a collection of named Fields. It is used as the basis of the
 * Record and can also be nested inside other Structures through the use of
 * StructureFields.
 *
 * Structures function as both the containers of information and the definition
 * of information. This especially holds true for Records where each instance
 * contains a unique instance of a Structure, but where the Structure of a Record
 * is also used to validate, generate and rewrite queries.
 *
 * @author Gido Hakvoort <gido@hakvoort.it>
 * @author Michiel Hakvoort <michiel@hakvoort.it>
 */
class Structure implements ArrayAccess {

    public $fields = array();

    /**
     * Define a new Structure.
     */
    public function __construct() {

    }

    /**
     * Add a Field to the structure.
     *
     * @param string $name The name of the Field to be added
     * @param Field $field The Field to be added
     *
     * @return \mongrove\Structure
     */
    public function addField($name, Field $field) {
        $this->fields[mb_strtolower($name)] = $field;
        return $this;
    }

    /**
     *
     * Check whether the Structure has a Field with the given name.
     * The name may also refer to sub fields, denoted by a separating dot.
     *
     * @param $name The name of the Field to check
     *
     * @return boolean True when the record has a Field of this name
     */
    public function hasField($name) {
        if(($idx = mb_strpos($name, '.')) === false) {
            return isset($this->fields[mb_strtolower($name)]);
        }

        $field = mb_substr($name, 0, $idx);

        if(!isset($this->fields[$field])) {
            return false;
        }

        if(!($this->fields[$field] instanceof CompositeField)) {
            return false;
        }

        return $this->fields[$field]->hasField(mb_substr($name, $idx+1));
    }

    /**
     * Get the Field with the given name.
     *
     * @param string $name
     *
     * @return Field|null
     */
    public function getField($name) {
        if(($idx = mb_strpos($name, '.')) === false) {
            return $this->fields[mb_strtolower($name)];
        }

        $field = mb_substr($name, 0, $idx);

        if(!isset($this->fields[$field])) {
            return null;
        }

        if(!($this->fields[$field] instanceof CompositeField)) {
            return null;
        }

        return $this->fields[$field]->getField(mb_substr($name, $idx+1));
    }

    /**
     * Get all Fields within this Structure.
     *
     * @return array[Field]
     */
    public function getFields() {
        return $this->fields;
    }

    /**
     * Dehydrate the Record to its Mongo representation.
     *
     * @return array The Mongo representation of the Record
     */
    public function dehydrate() {
        $result = array();

        foreach($this->fields as $name => $field) {
            $result[$name] = $field->dehydrate();
        }

        return $result;
    }

    /**
     * Hydrate the Record with the given Mongo representation.
     *
     * @param array $value
     */
    public function hydrate($value) {
        foreach($this->fields as $name => $field) {
            if(isset($value[$name])) {
                $field->hydrate($value[$name]);
            }
        }
    }

    /**
     * Return the mutations which are required to execute
     * in order to store any modifications in this Structure and its
     * underlying Fields in the Mongo document.
     *
     * @param string $path The path preceding this Structure in the document
     * @param string $name The name by which the Structure is known in its container
     *
     * @return array The collection of mutations in the Structure
     */
    public function getMutations($path = null, $name = null) {
        if($path !== null && $name !== null) {
            $path = $path.'.'.$name;
        } else if($name !== null) {
            $path = $name;
        }

        $mutations = array();

        foreach($this->fields as $name => $field) {
            $setMutations = $field->getMutations($path, $name);

            foreach($setMutations as $cycle => $cycleMutations) {
                if(!isset($mutations[$cycle])) {
                    $mutations[$cycle] = $cycleMutations;
                } else {
                    $mutations[$cycle] = array_merge_recursive($mutations[$cycle], $cycleMutations);
                }
            }
        }

        return $mutations;
    }

    /**
     * Set the Structure's state to clean. A cleaned Structure
     * will have no pending mutations which need to be executed
     * in order for the Structure's content to be persisted.
     */
    public function clean() {
        foreach($this->fields as $name => $field) {
            $field->clean();
        }
    }

    /**
     * Get an underlying value contained in a Field from this Structure.
     *
     * @param string $name
     *
     * @throws \Exception Thrown when a Field with the given name does not exist
     *
     * @return mixed
     */
    public function __get($name) {
        $name = mb_strtolower($name);
        if(isset($this->fields[$name])) {
            return $this->fields[$name]->getValue();
        } else {
            throw new Exception("Field : '{$name}' does not exist.");
        }
    }

    /**
     * Set the underlying value in the Field contained in this Structure.
	 *
     * @param string $name The name of the Field to set
     * @param mixed $value The value to Field should assume
     * 
     * @throws \Exception Thrown when a Field with the given name does not exist
     */
    public function __set($name, $value) {
        $name = mb_strtolower($name);
        if(isset($this->fields[$name])) {
            $this->fields[$name]->setValue($value);
        } else {
            throw new Exception("Field : '{$name}' does not exist.");
        }
    }

    /*
     * ArrayAccess Methods implementations
     */

    /**
     * (non-PHPdoc)
     * @see ArrayAccess::offsetExists()
     */
    public function offsetExists($offset) {
        $offset = mb_strtolower($offset);
        return isset($this->fields[$offset]);
    }

    /**
     * (non-PHPdoc)
     * @see ArrayAccess::offsetGet()
     */
    public function offsetGet($offset) {
        $offset = mb_strtolower($offset);
        if(isset($this->fields[$offset])) {
            return $this->fields[$offset];
        }
    }

    /**
     * (non-PHPdoc)
     * @see ArrayAccess::offsetSet()
     */
    public function offsetSet($offset, $value) {
        $offset = mb_strtolower($offset);
        if(isset($this->fields[$offset])) {
            $this->fields[$offset] = $value;
        }
    }

    /**
     * (non-PHPdoc)
     * @see ArrayAccess::offsetUnset()
     */
    public function offsetUnset($offset) {
        $offset = mb_strtolower($offset);
        if(isset($this->fields[$offset])) {
            unset($this->fields[$offset]);
        }
    }

    /**
     * Clone the Structure's underlying Fields
     */
    public function __clone() {
        foreach($this->fields as $name => $field) {
            $this->fields[$name] = clone $field;
        }
    }
}